﻿using System;
using System.Linq;
using System.Xml;
using Notoric.Model.Collections;
using Notoric.Model.Internal;
using Notoric.Model.Packaging;
using Notoric.Model.Validation;

namespace Notoric.Model.Elements
{
    /// <summary>
    /// Represents one measure (bar) of a composition.
    /// </summary>
    public class MeasureElement : ModelElement, IComparable<MeasureElement>
    {
        #region Constructors

        /// <summary>
        /// Creates a new <see cref="MeasureElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="CompositionElement"/>
        /// </para>
        /// <para>
        /// The composition that directly owns this measure.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="packagePart"/> or <paramref name="owner"/> is <see langword="null"/>.
        /// </exception>
        internal MeasureElement(CompositionPart packagePart, CompositionElement owner)
            : base(packagePart, owner)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            numberAttribute.Value = 0.ToString(PackageVocabulary.PackageCulture.NumberFormat);
            repetitionAttribute.Value = 0.ToString(PackageVocabulary.PackageCulture.NumberFormat);

            partsCollection = new ElementCollection<PartElement>(this, partsElement);
            parts = new ReadOnlyCollection<PartElement>(partsCollection);
        }

        /// <summary>
        /// Creates a new <see cref="MeasureElement"/> instance.
        /// </summary>
        /// <param name="packagePart">
        /// <para>
        /// Type: <see cref="CompositionPart"/>
        /// </para>
        /// <para>
        /// The package part that owns this element.
        /// </para>
        /// </param>
        /// <param name="owner">
        /// <para>
        /// Type: <see cref="CompositionElement"/>
        /// </para>
        /// <para>
        /// The composition that directly owns this measure.
        /// </para>
        /// </param>
        /// <param name="element">
        /// <para>
        /// Type: <see cref="XmlElement"/>
        /// </para>
        /// <para>
        /// The XML element that holds the data of this object.
        /// </para>
        /// </param>
        /// <exception cref="ArgumentNullException">
        /// Any of the arguments is <see langword="null"/>.
        /// </exception>
        internal MeasureElement(CompositionPart packagePart, CompositionElement owner, XmlElement element)
            : base(packagePart, owner, element)
        {
            if (owner == null)
                throw new ArgumentNullException("owner");

            numberAttribute = element.Attributes[PackageVocabulary.NumberAttribute];
            repetitionAttribute = element.Attributes[PackageVocabulary.RepetitionAttribute];

            meterElement = element[PackageVocabulary.MeterElementName, PackageVocabulary.PackageNamespaceUri];
            if (meterElement.HasChildNodes)
                meter = new TimeSignatureElement(PackagePart, this, (XmlElement)meterElement.FirstChild);

            partsElement = element[PackageVocabulary.PartElementName + "s", PackageVocabulary.PackageNamespaceUri];
            partsCollection = new ElementCollection<PartElement>(this, partsElement);
            parts = new ReadOnlyCollection<PartElement>(partsCollection);
        }

        #endregion

        #region ModelElement Members

        /// <inheritdoc/>
        protected override XmlElement CreateObjectElement(XmlDocument document)
        {
            var element = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.MeasureElementName,
                PackageVocabulary.PackageNamespaceUri
                );

            meterElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.MeterElementName,
                PackageVocabulary.PackageNamespaceUri
                );
            partsElement = document.CreateElement(
                PackageVocabulary.PackageNamespacePrefix,
                PackageVocabulary.PartElementName + "s",
                PackageVocabulary.PackageNamespaceUri
                );

            numberAttribute = document.CreateAttribute(PackageVocabulary.NumberAttribute);
            repetitionAttribute = document.CreateAttribute(PackageVocabulary.RepetitionAttribute);

            element.Attributes.Append(numberAttribute);
            element.Attributes.Append(repetitionAttribute);

            element.AppendChild(meterElement);
            element.AppendChild(partsElement);

            return element;
        }

        #endregion

        #region IComparable<MeasureElement> Members

        /// <inheritdoc/>
        public int CompareTo(MeasureElement other)
        {
            return Number - other.Number;
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Creates a new <see cref="ChordElement"/> and adds it to a given beat of a given voice of a given part.
        /// </summary>
        /// <param name="part">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the part to which the new symbol will be added.
        /// </para>
        /// </param>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The identifier (number) of the voice to which the symbol will be added.
        /// </para>
        /// </param>
        /// <param name="beat">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The beat to which the new symbol will be added.
        /// It is given in multiples of 128-th beats (0 stands for the first beat, 32 for the second beat /1 quarter note/ etc.).
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the new symbol.
        /// It is given in multiples of 128-th beats (1 is a 128-th note, 32 a quarter note, 128 a whole note, etc.)
        /// </para>
        /// </param>
        /// <param name="customData">
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="CustomSymbolData"/>
        /// </para>
        /// <para>
        /// An array of custom data extensions that will be added to the new symbol.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="ChordElement"/>
        /// </para>
        /// <para>
        /// The created chord element.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="part"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// <paramref name="duration"/> is 0.
        /// </exception>
        public ChordElement AddChord(PartDescriptorElement part, byte voice, ushort beat, ushort duration, CustomSymbolData[] customData)
        {
            if (part == null)
                throw new ArgumentNullException("part");

            var pe = partsCollection.FirstOrDefault(p => p.Descriptor == part);
            if (pe == null)
            {
                pe = new PartElement((CompositionPart)PackagePart, this, part);
                partsCollection.Add(pe);
            }
            var vd = pe.GetVoiceDescriptor(voice);

            return pe.Content.AddChord(beat, vd, duration, customData);
        }

        /// <summary>
        /// Creates a new <see cref="RestElement"/> and adds it to a given beat of a given voice of a given part.
        /// </summary>
        /// <param name="part">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the part to which the new symbol will be added.
        /// </para>
        /// </param>
        /// <param name="voice">
        /// <para>
        /// Type: <see cref="Byte"/>
        /// </para>
        /// <para>
        /// The identifier (number) of the voice to which the symbol will be added.
        /// </para>
        /// </param>
        /// <param name="beat">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The beat to which the new symbol will be added.
        /// It is given in multiples of 128-th beats (0 stands for the first beat, 32 for the second beat /1 quarter note/ etc.).
        /// </para>
        /// </param>
        /// <param name="duration">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The duration of the new symbol.
        /// It is given in multiples of 128-th beats (1 is a 128-th note, 32 a quarter note, 128 a whole note, etc.)
        /// </para>
        /// </param>
        /// <param name="customData">
        /// <para>
        /// Type: <see cref="Array"/> of <see cref="CustomSymbolData"/>
        /// </para>
        /// <para>
        /// An array of custom data extensions that will be added to the new symbol.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="RestElement"/>
        /// </para>
        /// <para>
        /// The created rest element.
        /// </para>
        /// </returns>
        /// <exception cref="ArgumentNullException">
        /// <paramref name="part"/> is <see langword="null"/>.
        /// </exception>
        /// <exception cref="ValidationException">
        /// <paramref name="duration"/> is 0.
        /// </exception>
        public RestElement AddRest(PartDescriptorElement part, byte voice, ushort beat, ushort duration, CustomSymbolData[] customData)
        {
            if (part == null)
                throw new ArgumentNullException("part");

            var pe = partsCollection.FirstOrDefault(p => p.Descriptor == part);
            if (pe == null)
            {
                pe = new PartElement((CompositionPart)PackagePart, this, part);
                partsCollection.Add(pe);
            }
            var vd = pe.GetVoiceDescriptor(voice);

            return pe.Content.AddRest(beat, vd, duration, customData);
        }

        /// <summary>
        /// Gets a beat with a given beginning in a given part or creates it if it does not exist.
        /// </summary>
        /// <param name="part">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the part at which the beat is being searched.
        /// </para>
        /// </param>
        /// <param name="beginning">
        /// <para>
        /// Type: <see cref="UInt16"/>
        /// </para>
        /// <para>
        /// The beginning of the beat.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="BeatElement"/>
        /// </para>
        /// <para>
        /// The searched beat.
        /// </para>
        /// </returns>
        public BeatElement GetOrCreateBeat(PartDescriptorElement part, ushort beginning)
        {
            var pe = partsCollection.FirstOrDefault(p => p.Descriptor == part);
            if (pe == null)
            {
                pe = new PartElement((CompositionPart)PackagePart, this, part);
                partsCollection.Add(pe);
            }
            var beat = pe.Content[0];
            if (beat == null)
                beat = pe.Content.AddBeat(0);

            return beat;
        }

        #endregion

        #region Public Properties

        /// <summary>
        /// Gets the part associated with a given descriptor.
        /// </summary>
        /// <param name="partDescriptor">
        /// <para>
        /// Type: <see cref="PartDescriptorElement"/>
        /// </para>
        /// <para>
        /// The descriptor of the requested part.
        /// </para>
        /// </param>
        /// <returns>
        /// <para>
        /// Type: <see cref="PartElement"/>
        /// </para>
        /// <para>
        /// The part element associated with the given descriptor or <see langword="null"/> if no such exists.
        /// </para>
        /// </returns>
        public PartElement this[PartDescriptorElement partDescriptor]
        {
            get { return parts.FirstOrDefault(p => p.Descriptor == partDescriptor); }
        }

        /// <summary>
        /// Gets or sets the meter of this measure.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="TimeSignatureElement"/>
        /// </para>
        /// <para>
        /// The meter of this measure. This value may be <see langword="null"/> in which case
        /// the measure does not have a defined meter (used in some modern compositions).
        /// </para>
        /// </value>
        /// <seealso cref="TimeSignatureElement"/>
        public TimeSignatureElement Meter
        {
            get { return meter; }
            set
            {
                if (meter != null)
                    meterElement.RemoveChild(meter.Element);
                meter = value;
                if (meter != null)
                    meterElement.AppendChild(meter.Element);
                
                RaisePropertyChanged("Meter");
            }
        }

        /// <summary>
        /// Gets or sets the number of the measure.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="System.UInt16"/>
        /// </para>
        /// <para>
        /// The one-based index of the measure within the composition.
        /// </para>
        /// </value>
        public ushort Number
        {
            get { return ushort.Parse(numberAttribute.Value, PackageVocabulary.PackageCulture.NumberFormat); }
            internal set
            {
                numberAttribute.Value = value.ToString(PackageVocabulary.PackageCulture.NumberFormat);
                RaisePropertyChanged("Number");
            }
        }

        /// <summary>
        /// Gets the collection of parts in this measure.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="ReadOnlyCollection{T}"/> of <see cref="PartElement"/>
        /// </para>
        /// <para>
        /// The collection of parts in this measure.
        /// </para>
        /// </value>
        public ReadOnlyCollection<PartElement> Parts
        {
            get { return parts; }
        }

        /// <summary>
        /// Gets or sets the repetition that this measure is a part of.
        /// </summary>
        /// <value>
        /// <para>
        /// Type: <see cref="RepetitionElement"/>
        /// </para>
        /// <para>
        /// A reference to the <see cref="RepetitionElement"/> that this measure is a part of.
        /// If the value is <see langword="null"/> the measure is not a part of any repetition.
        /// </para>
        /// </value>
        public RepetitionElement Repetition
        {
            get 
            {
                if (repetition == null && repetitionAttribute.Value != "0")
                    repetition = PackagePart.Package.IDMap.FindObject<RepetitionElement>(new Guid(repetitionAttribute.Value));
                
                return repetition; 
            }
            set
            {
                repetition = value;

                (PackagePart as CompositionPart).RaiseGlobalChangePending(this, this);

                if (repetition != null)
                    repetitionAttribute.Value = repetition.ID.ToString();
                else
                    repetitionAttribute.Value = "0";

                RaisePropertyChanged("Repetition");
            }
        }

        #endregion

        #region Private Fields

        /// <summary>
        /// Holds the value of the <see cref="Meter"/> property.
        /// </summary>
        private TimeSignatureElement meter;
        /// <summary>
        /// Holds the XML element that holds the value of the <see cref="Meter"/> property.
        /// </summary>
        private XmlElement meterElement;
        /// <summary>
        /// Holds the XML attribute that holds the value of the <see cref="Number"/> property.
        /// </summary>
        private XmlAttribute numberAttribute;
        /// <summary>
        /// Holds the value of the <see cref="Parts"/> property.
        /// </summary>
        private ReadOnlyCollection<PartElement> parts;
        /// <summary>
        /// Holds the collection of parts in this measure.
        /// </summary>
        private ElementCollection<PartElement> partsCollection;
        /// <summary>
        /// Holds the XML element that holds the value of the <see cref="Parts"/> property.
        /// </summary>
        private XmlElement partsElement;
        /// <summary>
        /// Holds the value of the <see cref="Repetition"/> property.
        /// </summary>
        private RepetitionElement repetition;
        /// <summary>
        /// Holds the XML attribute that holds the ID of the repetition refered by the <see cref="Repetition"/> property.
        /// </summary>
        private XmlAttribute repetitionAttribute;

        #endregion
    }
}
